package org.tczs93.lunce.lucene;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

/**
 * @author ：tao.li
 * @description：lucene索引的创建和根据索引查询数据
 * @date ：Created in 2021/9/3 15:23
 */
@Component
public class LuceneIndex {
    Logger logger = LoggerFactory.getLogger(LuceneIndex.class);

    @Value("${System.lucene.dataDir}")
    private String dataDir;

    /**
     * 插入文档
     * 没有使用LuceneDocument注解，文档名默认为类名
     * 使用了LuceneDocument却没有指定name,文档名默认为类名,否则为指定名
     * @param obj 文档数据对象
     * @return 0为插入失败，大于0为成功
     */
    public Integer insertDocument(Object obj){
        if(obj == null)
            return 0;
        String documentName = getDocumentName(obj);
        return insertDocument(documentName,obj);
    }

    /**
     * 插入文档
     * @param documentName 指定插入的文档名称
     * @param obj 文档数据对象
     * @return  0为插入失败，大于0为成功
     */
    public Integer insertDocument(String documentName,Object obj) {
        if(obj == null)
            return 0;
        List<Object> list = new ArrayList<>();
        list.add(obj);
        return insertBatchDocument(documentName,list);
    }

    /**
     * 插入文档
     * 获取集合中的第一个对象判断是否使用LuceneDocument注解
     * 没有使用LuceneDocument注解，文档名默认为类名
     * 使用了LuceneDocument却没有指定name,文档名默认为类名,否则为指定名
     * @param list 文档数据对象集合
     * @return 0为插入失败，大于0为成功
     */
    public <T> Integer insertBatchDocument(List<T> list){
        if(CollectionUtils.isEmpty(list))
            return 0;
        String documentName = getDocumentName(list.get(0));
        return insertBatchDocument(documentName,list);
    }

    /**
     * 批量插入文档
     * @param documentName 指定插入的文档名称
     * @param list 文档数据对象集合
     * @return 0为插入失败，大于0为成功
     */
    public <T> Integer insertBatchDocument(String documentName,List<T> list){
        if(CollectionUtils.isEmpty(list))
            return 0;
        long startTime = System.currentTimeMillis();
        IndexWriter indexWriter = createIndexWriter(documentName);
        int num = 0;
        try {
            for (Object obj : list) {
                Document doc = ObjectConvert.convertDocument(obj);
                indexWriter.addDocument(doc);
            }
            indexWriter.commit();
            num = indexWriter.numDocs();
            logger.info("共索引了"+num+"个文件");
        }catch (Exception e){
            logger.error("create lucene index failed:",e);
        }finally {
            closeIndexWriter(indexWriter);
        }
        long endTime = System.currentTimeMillis();
        if(num == 0)
            logger.info("创建索引失败,所用时间:"+(endTime-startTime)+"毫秒");
        else
            logger.error("创建索引成功,所用时间:"+(endTime-startTime)+"毫秒");
        return num;
    }

    //默认查询结果高亮显示
    public <T> List<T> findDocument(String column,String value,Class<T> t){
        return findDocument(getDocumentName(t),column,value,t);
    }

    //指定文档名称,默认查询结果高亮显示
    public <T> List<T> findDocument(String documentName,String column,String value,Class<T> t){
        return findDocument(documentName,column,value,t,true);
    }

    /**
     * 根据条件查询文档
     * @param documentName 文档名
     * @param column 查询字段
     * @param value 查询值
     * @param t 返回值类型
     * @param highlighterFlag 是否高亮显示
     * @param <T> 泛型
     * @return 返回条件查询结果
     */
    public <T> List<T> findDocument(String documentName,String column,String value,Class<T> t,boolean highlighterFlag) {
        String name = dataDir+ File.separator+documentName;
        long startTime = System.currentTimeMillis();
        DirectoryReader reader = null;
        IndexSearcher searcher = null;
        Analyzer analyzer = null;
        Query query = null;
        TopDocs docs = null;
        try {
            Directory directory = FSDirectory.open(Paths.get(name));
            reader = DirectoryReader.open(directory);
            searcher = new IndexSearcher(reader);
            analyzer = new SmartChineseAnalyzer();
            QueryParser parser = new QueryParser(column, analyzer);
            query = parser.parse(value);
            docs = searcher.search(query, 10);
        }catch (Exception e){
            logger.error("lucene find document failed:",e);
        }
        long endTime = System.currentTimeMillis();
        List<T> tList = new ArrayList<>();
        if(docs == null){
            logger.error("查找 "+value+" 失败,所用时间:"+(endTime-startTime)+"毫秒");
        }else{
            logger.info("查找 "+value+" 成功,所用时间:"+(endTime-startTime)+"毫秒");
            logger.info("查询到"+docs.totalHits+"条记录");
            try{
                if(highlighterFlag)
                    tList = getHighlighterDocument(docs,t,query,searcher,analyzer,column);
                else
                    tList = getCommonDocument(docs,searcher,t);
            }catch (Exception e){
                logger.error("lucene find document convert failed:",e);
            }
        }
        if(reader != null){
            try {
                reader.close();
            } catch (IOException e) {
                logger.error("lucene close DirectoryReader failed:",e);
            }
        }
        return tList;
    }

    public <T> List<T> findDocument(String documentName,Analyzer analyzer,Query query,String column,Class<T> t,boolean highlighterFlag) {
        String name = dataDir+ File.separator+documentName;
        long startTime = System.currentTimeMillis();
        DirectoryReader reader = null;
        IndexSearcher searcher = null;
        TopDocs docs = null;
        try {
            Directory directory = FSDirectory.open(Paths.get(name));
            reader = DirectoryReader.open(directory);
            searcher = new IndexSearcher(reader);
            docs = searcher.search(query, 10);
        }catch (Exception e){
            logger.error("lucene find document failed:",e);
        }
        long endTime = System.currentTimeMillis();
        List<T> tList = new ArrayList<>();
        if(docs == null){
            logger.error("查找失败,所用时间:"+(endTime-startTime)+"毫秒");
        }else{
            logger.info("查找成功,所用时间:"+(endTime-startTime)+"毫秒");
            logger.info("查询到"+docs.totalHits+"条记录");
            try{
                if(highlighterFlag)
                    tList = getHighlighterDocument(docs,t,query,searcher,analyzer,column);
                else
                    tList = getCommonDocument(docs,searcher,t);
            }catch (Exception e){
                logger.error("lucene find document convert failed:",e);
            }
        }
        if(reader != null){
            try {
                reader.close();
            } catch (IOException e) {
                logger.error("lucene close DirectoryReader failed:",e);
            }
        }
        return tList;
    }

    /**
     * 查询的字段结果匹配内容高亮显示
     * @param docs
     * @param t
     * @param query
     * @param searcher
     * @param analyzer
     * @param column 查询字段
     * @param <T>
     * @return
     * @throws IOException
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws InvalidTokenOffsetsException
     */
    private <T> List<T> getHighlighterDocument(TopDocs docs,Class<T> t,Query query,IndexSearcher searcher,Analyzer analyzer,String column) throws IOException, InstantiationException, IllegalAccessException, InvalidTokenOffsetsException {
        //加入高亮显示的
        SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color=red>","</font></b>");
        QueryScorer scorer = new QueryScorer(query);//计算查询结果最高的得分
        Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);//根据得分算出一个片段
        Highlighter highlighter = new Highlighter(simpleHTMLFormatter,scorer);
        highlighter.setTextFragmenter(fragmenter);//设置显示高亮的片段
        List<T> tList = new ArrayList<>();
        for(ScoreDoc scoreDoc : docs.scoreDocs){
            Document doc = searcher.doc(scoreDoc.doc);
            T t1 = convertT(doc,t);
            String columnValue = doc.get(column);
            if(columnValue != null){
                TokenStream tokenStream =  analyzer.tokenStream("title", new StringReader(columnValue));
                columnValue = highlighter.getBestFragment(tokenStream, columnValue);
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(t1);
                bw.setPropertyValue(column,columnValue);
            }
            tList.add(t1);
        }
        return tList;
    }

    /**
     * 普通查询
     * @param docs
     * @param searcher
     * @param t
     * @param <T>
     * @return
     * @throws InstantiationException
     * @throws IllegalAccessException
     * @throws IOException
     */
    private <T> List<T> getCommonDocument(TopDocs docs,IndexSearcher searcher,Class<T> t) throws InstantiationException, IllegalAccessException, IOException {
        List<T> tList = new ArrayList<>();
        for(ScoreDoc scoreDoc : docs.scoreDocs){
            Document doc = searcher.doc(scoreDoc.doc);
            T t1 = convertT(doc,t);
            tList.add(t1);
        }
        return tList;
    }

    private IndexWriter createIndexWriter(String documentName){
        String dataDirName = dataDir+ File.separator+documentName;
        Analyzer analyzer = new SmartChineseAnalyzer();
        IndexWriter indexWriter = null;
        try {
            Directory directory = FSDirectory.open(Paths.get(dataDirName));
            IndexWriterConfig config = new IndexWriterConfig(analyzer);
            indexWriter = new IndexWriter(directory, config);
        }catch (IOException e){
            logger.error("lucene IndexWriter create failed:",e);
        }
        return indexWriter;
    }

    private void closeIndexWriter(IndexWriter indexWriter){
        if(indexWriter != null){
            try {
                indexWriter.close();
            } catch (IOException e) {
                logger.error("lucene IndexWriter close failed:",e);
            }
        }
    }

    private String getDocumentName(Object obj){
        Class<?> aClass = obj.getClass();
        return getDocumentName(aClass);
    }

    private String getDocumentName(Class<?> t){
        LuceneDocument annotation = t.getAnnotation(LuceneDocument.class);
        String documentName;
        if(annotation == null)
            documentName = t.getSimpleName();
        else if((documentName = annotation.name()).length() == 0)
            documentName = t.getSimpleName();
        return documentName;
    }

    private <T> T convertT(Document doc,Class<T> t) throws IllegalAccessException, InstantiationException {
        T o = (T) t.newInstance();
        java.lang.reflect.Field[] fields = t.getDeclaredFields();
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(o);
        for(java.lang.reflect.Field field : fields){
            String fieldName = field.getName();
            String value = doc.get(fieldName);
            bw.setPropertyValue(fieldName,value);
        }
        return o;
    }

    public static class ObjectConvert{

        static Document convertDocument(Object obj){
            Document doc = new Document();
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(obj);
            Class<?> clazz = obj.getClass();
            java.lang.reflect.Field[] fields = clazz.getDeclaredFields();
            for(java.lang.reflect.Field field : fields){
                String fieldName = field.getName();
                Class<?> type = field.getType();
                Object value = bw.getPropertyValue(fieldName);
                buildDocument(doc,value,fieldName,type);
            }
            return doc;
        }

        private static void buildDocument(Document doc, Object value, String fieldName, Class type) {
            if(value == null)
                return;
            Field field;
            if(type.equals(Integer.class) || type.equals(int.class))
                field = new IntField(fieldName, (Integer) value, Field.Store.YES);
            else if(type.equals(Long.class) || type.equals(long.class))
                field = new LongField(fieldName, (Long) value, Field.Store.YES);
            else if(type.equals(Float.class) || type.equals(float.class))
                field = new FloatField(fieldName, (Float) value, Field.Store.YES);
            else if(type.equals(Double.class) || type.equals(double.class))
                field = new DoubleField(fieldName, (Double) value, Field.Store.YES);
            else if(type.equals(String.class))
                field = new TextField(fieldName, (String) value,Field.Store.YES);
            else
                throw new IllegalArgumentException("lucene insert no mapper filed type,type support Integer,Long," +
                        "Float,Double,String");
            doc.add(field);
        }
    }
}
